<?php
/**
 * 数据库助手：方便对数据库表进行操作。包括：对输入数据进行筛选、过滤，方
 *             便Zend_Db_adapter或者Zend_Db_Table对象使用；验证输入数据的
 *             有效性；对表的字段进行格式化，以方便显示（直接显式或用view
 *             显式）；与相关的js代码配合使用可以生成ajax风格代码。
 *
 * 注意：暂时只适用于mySQL数据库。
 *
 * @author  wuxi dot cn at gmail dot com
 * @version 0.1 2007.12
 */
class db_helper
{
	protected $that=null;
	protected $table=''; // 表名
	protected $fields=array(); // 表的字段属性：
					// array('field'=><property>, ...)
					// property: array(attr, <check>, html, editor)
					// check: array(type, min, max, chars)
	protected $pk=null; // primary key's field name
	// protected $null=''; // 表中可以为空的字段，string, 用|分隔。

	/// 出错字段。string
	public $field;
	/// 出错信息提示。string
	public $error;

	/**
	 * 分析一个field属性。返回array(name, null, pk, attr)。
	 */
	protected function parse($field)
	{
		$ret=array();
		list($name, $check, $html, $editor)=explode('|', $field.'|*|*|*');

		// name; pk, null, not null
		list($ret[0], $ret[1])=split('[\(\)]', $name.'(not null)');

		// check, array('type', 'min', 'max', 'chars')
		list($type, $tmp)=explode('(', $check.'(');
		list($min, $max, $chars)=split('[,)]', $tmp.',,');
		$chars=empty($chars)?'.':$chars;
		if (empty($type) || $type=='*')
			$type='string';
		else if ($type=='time') {
			$min=strtotime($min);
			$max=strtotime($max);
		}
		if (empty($min)) $min=0;
		if (empty($max)) $max=PHP_INT_MAX;
		//if ($max<$min) $max=$min;
		if (empty($chars))
			$chars='.';
		$ret[2]=array('type'=>$type, 'min'=>$min, 'max'=>$max,
					'chars'=>$chars);

		// html
		$ret[3]=$html;
		if (empty($ret[3]) || $ret[3]=='*')
			$ret[3]='_text';

		// editor
		$ret[4]=$editor;
		if (empty($ret[4]) || $ret[4]=='*') {
			if ($type=='time')
				$ret[4]=$ret[3];
			else
				$ret[4]='_input';
		}

		return $ret;
	}

	/**
	 * 构造函数。详细见文档。
	 *
	 * @param that   object, 包含处理函数的对象。
	 * @param table  string, 表名。
	 * @param fields string, 字段约束。
	 */
	public function __construct($that, $table, $fields)
	{
		$this->that=$that;
		$this->table=$table;

		$tmp=explode('||', $fields);
		foreach ($tmp as $f) {

			list($n, $attr, $check, $html, $editor)=self::parse($f);
			$this->fields[$n]=array('attr'=>$attr,
									'check'=>$check,
									'html'=>$html,
									'editor'=>$editor);

			if ($attr=='pk') {
				if (empty($this->pk))
					$this->pk=$n;
				else
					throw new Exception("Failed on field `$n`: ".
						"more than one pk in `$table`. ");
			}

		} // end of foreach

		// check if has pk
		if (empty($this->pk))
			throw new Exception("`$table` must have a primary key. ");
	
	}

	/**
	 * 从数组中过滤出表中字段。
	 *
	 * @param data  array, 待处理的数据。
	 * @param limit bool, 筛选结果数目限制，=0为不限制。
	 *
	 * @return array, 过滤后只包含表里出现的字段的数组。
	 */
	public function filter($data, $limit=0)
	{
		$result=array();
		$n=0;
		$limit=$limit==0?9999:$limit;
		foreach ($data as $k => $v) {
			if (array_key_exists($k, $this->fields))
				$result[$k]=$v;
			if (++$n>=$limit) break;
		}
		return $result;
	}

	/**
	 * 通过$check里的规则检测数据$data。
	 * check: array(type, min, max, chars)
	 * 通过检查返回true，否则返回false。
	 */
	private static function test($check, $data)
	{
		switch ($check['type']) {
		case 'int':
			if ($data<$check['min'] || $data>$check['max'])
				return false;
			break;
		case 'string':
			if (strlen($data)<$check['min'] || strlen($data)>$check['max'])
				return false;
			if (!preg_match('/'.$check['chars'].'+/', $data))
				return false;
			break;
		case 'time':
			if (!is_int($data)) $data=strtotime($data);
			if ($data<$check['min'] || $data>$check['max'])
				return false;
			break;
		default:
			return false;
		}

		return true;
	}

	/**
	 * 过滤字段，并检测字段是否符合规范。
	 *
	 * @param data   array, 待检测的数组。
	 * @param ignore bool, 跳过data里没有字段，不提示错误。
	 * 
	 * @return array|int, 成功返回array，失败返回错误码：
	 *         -1: 字段缺失。出错字段：$this->field，出错信息：$this->error
	 *         -2: 字段不符合规范。出错字段、出错信息同上。
	 */
	public function check($data, $ignore=false)
	{
		foreach ($this->fields as $k => $v) {
			if (array_key_exists($k, $data)) {
				if (!self::test($v['check'], $data[$k])) {
					$this->field=$k;
					$this->error='Checking failed in field "'.$k.'".';
					return -2;
				}
			}
			else if (!$ignore && $v['attr']=='not null') {
				$this->field=$k;
				$this->error="Field `$k` is missing. ";
				return -1;
			}
		} // end of foreach

		return $data;
	}

	/**
	 * 通过参数筛选出字段，并形成字符串，以便数据库操作。
	 *
	 * @param data    array, 待处理的数组。
	 * @param prefix  string, 字段的前缀，用于Zend_Db_Adapter。
	 *                例如，前缀可以是：':', 则字段为：':Field'.
	 * @param bracket string, 字段名前后的符号，为了数据库用。
	 *                例如='`'，则每个字段为：`Field`。
	 *
	 * @return string, 输出结果，格式："Field_1, Field_2, ..." 或：
	 *                 ":Field_1, :Field_2, ..." 或："`Field_1`, ..." 
	 */
	public function field_string(array $data, $bracket='', $prefix='')
	{
		$d=self::filter($data);
		$s='';
		$sep='';
		$b=$bracket;
		$p=$prefix;
		foreach ($d as $k => $v) {
			$s.=$sep.$b.$p.$k.$b;
			$sep=', ';
		}
		return $s;
	}

	// 用于escape和unescape函数。
	static protected $trans=array('<'=>'&lt;', '>'=>'&gt;',
			"'"=>'&#39;', '"'=>'&quot;', '&'=>'&amp;', ' '=>'&nbsp;',
			'/'=>'&#47;', "\n"=>'<br />', "\r"=>'',
			"\t"=>'&nbsp;&nbsp;&nbsp;&nbsp;'
			);

	/**
	 * 特殊符号=>HTML记号转换，类似php中的htmlentities函数。
	 *
	 * @param data string, 待转换的字符串。
	 *
	 * @return string, 转换后的结果。
	 */
	static function escape($data)
	{
		return strtr($data, self::$trans);
	}

	/**
	 * html记号=>特殊符号转换。self::escape()函数的逆函数。
	 *
	 * @param data string, 待转换的字符串。
	 *
	 * @return string, 转换后的结果。
	 */
	static function unescape($data)
	{
		$trans=array_flip(self::$trans);
		return strtr($data, $trans);
	}

	/**
	 * 格式化数据库字段。
	 *
	 * @param data array, 待格式化的数据库一行数据。
	 * @param fmt  array, 一些字段的特定格式函数。array('field'=>func...)。
	 * @param inst object, =null时使用默认的$this->that作为实例对象，!=null
	 *             时使用$inst指定的实例对象来调用格式化函数。
	 * @param edit bool, 决定是输出格式化还是以编辑器格式化。=false输出格式
	 *             化，=true编辑器格式化。
	 *
	 * @return, array, 格式化以后的数据。
	 */
	function format($data, $fmt=array(), $edit=false, $inst=null)
	{
		if (!is_array($fmt))
			throw new Exception('The 2nd parameter of function "format()"'.
					' must be an array, even if it\'s empty. ');

		$ret=array();
		$inst=empty($inst)?$this->that:$inst;		

		foreach ($data as $k => $v) {

			$s='';
			if (!array_key_exists($k, $this->fields))
				continue;
			
			// choose format function
			if ($edit) { // editor format
				$f=array_key_exists($k, $fmt)?
					$fmt[$k]:$this->fields[$k]['editor'];

				// deal with time
				if (substr($f, 0, 5)=='_time') {
					$tmp=substr($f, 6, -1);
					$v=strftime($tmp, $v);
					$f='_input';
				}
			}
			else { // html output format
				$f=array_key_exists($k, $fmt)?
					$fmt[$k]:$this->fields[$k]['html'];

				// deal with time
				if (substr($f, 0, 5)=='_time') {
					$tmp=substr($f, 6, -1);
					if (empty($v))
						$v='NULL';
					else
						$v=strftime($tmp, $v);
					$f='_text';
				}
			} // end of if edit

			// invoke format function
			if ($f[0]=='_') // call default function
				$s=self::$f($v);
			else // call user defined function
				$s=empty($inst)?$f($v):$inst->$f($v);

			$ret[$k]=$s;

		} // end of foreach

		return $ret;
	}

	// 格式化存文本。
	static public function _text($v)
	{
		return self::escape($v);
	}
	
	// 格式化存文本，但解释其中的连接地址。
	static public function _a($v)
	{
		$v=self::escape($v);
		return preg_replace('@((?:https?|mailto).*?)(&nbsp;|$)@',
							'<img src="/pub/image/a.gif" alt="连接:" />'.
							'<a href="\1" target="_blank">\1</a>',
							$v);
	}

	// 格式化安全html
	static public function _html($v)
	{
		return preg_replace('/<script([^<]+)<\/script([^>]*)>/',
				'&lt;script&gt;$1&lt;/script&gt;>', $v);
	}
	
	// 不处理文本
	static public function _pre($v)
	{
		return $v;
	}

	// input[text]输入框
	static public function _input($v)
	{
		return strtr($v, array("\n"=>'\n', "\r"=>'\r', '"'=>'&quot;'));
	}

	// textarea框
	static public function _textarea($v)
	{
		return $v;
	}

	/**
	 * 字符串转为unit时间绰，简单的实现php的strtotime，解决1935年问题。
	 * 输入格式为：yyyy-mm-dd HH:MM:SS
	 */
	static public function strtotime($s)
	{
		list($y, $m, $d, $H, $M, $S)=split('[-\s :]', $s.'-------');
		$y=intval($y);
		if ($y==0) return 0;
		$m=empty($m)?1:intval($m);
		$d=empty($d)?1:intval($d);
		$t=mktime(intval($H), intval($M), intval($S), $m, $d, $y);
		return empty($t)?1:$t;
	}

};
?>
